/********************************************************************
 * Copyright (c) 2006 Contributors. All rights reserved.
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Public License v 2.0
 * which accompanies this distribution and is available at
 * https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt
 *
 * Contributors: IBM Corporation - initial API and implementation
 * 				 Helen Hawkins   - initial version
 *******************************************************************/
package org.aspectj.systemtest.incremental.tools;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.aspectj.ajde.core.IOutputLocationManager;
import org.aspectj.ajdt.internal.core.builder.AjBuildConfig;
import org.aspectj.ajdt.internal.core.builder.AjState;
import org.aspectj.ajdt.internal.core.builder.IncrementalStateManager;
import org.aspectj.util.FileUtil;
import org.aspectj.weaver.bcel.UnwovenClassFile;

/**
 * Similar to OutputLocationManagerTests, however, tests the different scenarios when no outputDir is set but instead there is an
 * OutputLocationManager which returns the same output location for all files and resources.
 *
 * There are eight places where AjBuildConfig.getOutputDir() is called that are tested here:
 *
 * AjBuildManager.getOutputClassFileName(..) - testCorrectInfoWhenNoOutputPath AjBuildManager.initBcelWorld(..) -
 * testPathResolutionWithInpathDirAndNoOutputPath testPathResolutionWithInpathJarAndNoOutputPath AjBuildManager.writeManifest(..) -
 * testCopyManifest AjBuildManager.writeOutxml(..) - testOutxml - testOutXmlForAspectsWithDifferentOutputDirs
 * AjState.createUnwovenClassFile(..) - testPathResolutionAfterChangeInClassOnInpath AjState.deleteResources(..) -
 * testAjStateDeleteResources AjState.maybeDeleteResources(..) - testAjStateDeleteResourcesInInputDir
 * AjState.removeAllResultsOfLastBuild(..) - testAllResourcesAreDeletedCorrectlyOnPathChange
 * IncrementalStateManager.findStateManagingOutputLocation(..) - testFindStateManagingOutputLocation
 *
 * The other three places are not tested here because they were implemented when OutputLocationManager was introduced.
 *
 */
public class MoreOutputLocationManagerTests extends AbstractMultiProjectIncrementalAjdeInteractionTestbed {

	private String inpathTestingDir;
	private String expectedOutputDir;

	@Override
	protected void setUp() throws Exception {
		super.setUp();
		initialiseProject("inpathTesting");
		inpathTestingDir = getWorkingDir() + File.separator + "inpathTesting";
		expectedOutputDir = inpathTestingDir + File.separator + "bin";
		configureOutputLocationManager("inpathTesting", new SingleDirOutputLocMgr(inpathTestingDir));
	}

	/**
	 * Tests that the UnwovenClassFiles have the correct path when there is no outputDir but there is an OutputLocationManager. Is a
	 * simple project that has no inpath setting
	 */
	public void testCorrectInfoWhenNoOutputPath() {
		build("inpathTesting");
		AjState state = getState();

		Map<String,File> classNameToFileMap = state.getClassNameToFileMap();
		assertFalse("expected there to be classes ", classNameToFileMap.isEmpty());
		Set<Map.Entry<String,File>> entrySet = classNameToFileMap.entrySet();
		for (Map.Entry<String, File> entry : entrySet) {
			String className = entry.getKey();
			String fullClassName = expectedOutputDir + File.separator + className.replace('.', File.separatorChar) + ".class";
			File file = entry.getValue();
			assertEquals("expected file to have path \n" + fullClassName + ", but" + " found path \n" + file.getAbsolutePath(),
					fullClassName, file.getAbsolutePath());
		}
	}

	/**
	 * Tests that can retieve the state that manages a given output location when there is no outputDir set
	 */
	public void testFindStateManagingOutputLocation() {
		build("inpathTesting");
		AjState state = IncrementalStateManager.findStateManagingOutputLocation(new File(expectedOutputDir));
		assertNotNull("Expected to find a state that managed output location " + expectedOutputDir + ", but did not", state);

	}

	/**
	 * Tests that the UnwovenClassFiles corresponding to classes on the inpath have the correct class name when there is no output
	 * directory (ultimately tests AjBuildManager.initBcelWorld() when there is a jar on the inpath). Only does one build.
	 */
	public void testPathResolutionWithInpathDirAndNoOutputPath() {
		String inpathDir = inpathTestingDir + File.separator + "injarBin" + File.separator + "pkg";
		addInpathEntry(inpathDir);
		build("inpathTesting");

		// expect to compile the aspect in 'inpathTesting' project and weave
		// both the aspect and the class on the inpath.
		checkCompileWeaveCount("inpathTesting", 1, 2);

		// get hold of the state for this project - expect to find one
		AjState state = getState();

		// the classes onthe inpath are recorded against the AjBuildManager
		// (they are deleted from the ajstate whilst cleaning up after a build)
		Map<String,List<UnwovenClassFile>> binarySources = state.getAjBuildManager().getBinarySourcesForThisWeave();
		assertFalse("expected there to be binary sources from the inpath setting but didn't find any", binarySources.isEmpty());

		List<UnwovenClassFile> unwovenClassFiles = binarySources.get(inpathDir + File.separator + "InpathClass.class");
		List<String> fileNames = new ArrayList<>();
		// the unwovenClassFiles should have filenames that point to the output dir
		// (which in this case is the sandbox dir) and not where they came from.
		for (UnwovenClassFile ucf: unwovenClassFiles) {
			if (!ucf.getFilename().contains(expectedOutputDir)) {
				fileNames.add(ucf.getFilename());
			}
		}
		assertTrue("expected to find UnwovenClassFile from directory\n" + expectedOutputDir + ", \n but found files " + fileNames,
				fileNames.isEmpty());
	}

	/**
	 * Tests that the UnwovenClassFiles corresponding to classes on the inpath have the correct class name when there is no output
	 * directory (ultimately tests AjState.createUnwovenClassFile(BinarySourceFile) and ensures the unwovenClassFile has the correct
	 * name. Makes a change to a class file on the inpath to ensure we enter this method (there is a check that says are we the
	 * first build))
	 */
	public void testPathResolutionAfterChangeInClassOnInpath() throws Exception {
		String inpathDir = inpathTestingDir + File.separator + "injarBin" + File.separator + "pkg";
		addInpathEntry(inpathDir);
		build("inpathTesting");

		// build again so that we enter
		// AjState.createUnwovenClassFile(BinarySourceFile)
		File from = new File(testdataSrcDir + File.separatorChar + "inpathTesting" + File.separatorChar + "newInpathClass"
				+ File.separatorChar + "InpathClass.class");
		File destination = new File(inpathDir + File.separatorChar + "InpathClass.class");
		FileUtil.copyFile(from, destination);

		// get hold of the state for this project - expect to find one
		AjState state = getState();
		AjBuildConfig buildConfig = state.getBuildConfig();
		state.prepareForNextBuild(buildConfig);

		Map<String, List<UnwovenClassFile>> binarySources = state.getBinaryFilesToCompile(true);
		assertFalse("expected there to be binary sources from the inpath setting but didn't find any", binarySources.isEmpty());

		List<UnwovenClassFile> unwovenClassFiles = binarySources.get(inpathDir + File.separator + "InpathClass.class");
		List<String> fileNames = new ArrayList<>();
		// the unwovenClassFiles should have filenames that point to the output dir
		// (which in this case is the sandbox dir) and not where they came from.
		for (UnwovenClassFile ucf: unwovenClassFiles) {
			if (!ucf.getFilename().contains(expectedOutputDir)) {
				fileNames.add(ucf.getFilename());
			}
		}
		assertTrue("expected to find UnwovenClassFile from directory\n" + expectedOutputDir + ", \n but found files " + fileNames,
				fileNames.isEmpty());
	}

	/**
	 * Tests that the UnwovenClassFiles corresponding to jars on the inpath have the correct class name when there is no output path
	 * (ultimately tests AjBuildManager.initBcelWorld() when there is a jar on the inpath). Only does one build.
	 */
	public void testPathResolutionWithInpathJarAndNoOutputPath() {
		String inpathDir = inpathTestingDir + File.separator + "inpathJar.jar";
		addInpathEntry(inpathDir);
		build("inpathTesting");
		// expect to compile the aspect in 'inpathTesting' project and weave
		// both the aspect and the class in the jar on the inpath.
		checkCompileWeaveCount("inpathTesting", 1, 2);

		AjState state = getState();

		// tests AjState.createUnwovenClassFile(BinarySourceFile)
		Map<String,List<UnwovenClassFile>> binarySources = state.getAjBuildManager().getBinarySourcesForThisWeave();
		assertFalse("expected there to be binary sources from the inpath setting but didn't find any", binarySources.isEmpty());

		List<UnwovenClassFile> unwovenClassFiles = binarySources.get(inpathDir);
		List<String> fileNames = new ArrayList<>();

		for (UnwovenClassFile ucf: unwovenClassFiles) {
			if (!ucf.getFilename().contains(expectedOutputDir)) {
				fileNames.add(ucf.getFilename());
			}
		}
		assertTrue("expected to find UnwovenClassFile from directory\n" + expectedOutputDir + ", \n but found files " + fileNames,
				fileNames.isEmpty());

	}

	/**
	 * A manifest file is in the jar on the inpath - check that it's copied to the correct place
	 */
	public void testCopyManifest() {
		String inpathDir = inpathTestingDir + File.separator + "inpathJar.jar";
		addInpathEntry(inpathDir);

		build("inpathTesting");
		String resource = expectedOutputDir + File.separator + "META-INF" + File.separator + "MANIFEST.MF";
		File f = new File(resource);
		assertTrue("expected file " + resource + " to exist but it did not", f.exists());
	}

	/**
	 * "resources" are contained within inpath jars - check that a text file contained within a jar is copied and then deleted
	 * correctly. Essentially tests AjState.deleteResources().
	 */
	// see 243376: for now don't do this, waste of cpu - ajdt better for handling resources - but is that true for inpath resources?
	// public void testAjStateDeleteResources() {
	// String inpathDir = inpathTestingDir + File.separator + "inpathJar.jar";
	// addInpathEntry(inpathDir);
	//
	// build("inpathTesting");
	//
	// AjState state = getState();
	//
	// String resource = expectedOutputDir + File.separator + "inpathResource.txt";
	// File f = new File(resource);
	// assertTrue("expected file " + resource + " to exist but it did not",f.exists());
	// // this call should delete the resources
	// state.getFilesToCompile(true);
	// assertFalse("did not expect the file " + resource + " to exist but it does",f.exists());
	// }
	/**
	 * Can set to copy resources that are in inpath dirs - check that a text file contained within such a dir is copied and then
	 * deleted correctly. Essentially tests AjState.maybeDeleteResources().
	 */
	// see 243376: for now don't do this, waste of cpu - ajdt better for handling resources - but is that true for inpath resources?
	// public void testAjStateDeleteResourcesInInputDir() {
	// // temporary problem with this on linux, think it is a filesystem lastmodtime issue
	// if (System.getProperty("os.name","").toLowerCase().equals("linux")) return;
	// if (System.getProperty("os.name","").toLowerCase().indexOf("mac")!=-1) return;
	//
	// AjBuildManager.COPY_INPATH_DIR_RESOURCES = true;
	// try {
	// String inpathDir = inpathTestingDir + File.separator + "injarBin"
	// + File.separator + "pkg";
	// addInpathEntry(inpathDir);
	// build("inpathTesting");
	// AjState state = getState();
	// String resource = "inDirResource.txt";
	// assertTrue("expected state to have resource " + resource + "but it did not",
	// state.hasResource(resource));
	// // this call should delete the resources - tests AjState.deleteResources()
	// state.getFilesToCompile(true);
	// assertFalse("did not expect state to have resource " + resource +
	// " but found that it did", state.hasResource(resource));
	// } finally {
	// AjBuildManager.COPY_INPATH_DIR_RESOURCES = false;
	// }
	//
	// }
	/**
	 * Changing inpath entry from a jar to a directory between builds means that AjState should realise somethings changed. This
	 * causes all resources (Manifest and txt files) to be deleted. Also should be a full build. Essentially tests
	 * AjState.removeAllResultsFromLastBuild().
	 */
	public void testAllResourcesAreDeletedCorrectlyOnPathChange() {
		String inpathJar = inpathTestingDir + File.separator + "inpathJar.jar";

		addInpathEntry(inpathJar);
		build("inpathTesting");

		String resource = expectedOutputDir + File.separator + "inpathResource.txt";
		File f = new File(resource);
		assertTrue("expected file " + resource + " to exist but it did not", f.exists());

		// this should force a change and the file is deleted
		// tests AjState.removeAllResultsFromLastBuild()
		addInpathEntry(null);
		build("inpathTesting");
		assertFalse("did not expect the file " + resource + " to exist but it does", f.exists());

		checkWasFullBuild();
	}

	public void testOutxml() {
		configureNonStandardCompileOptions("inpathTesting", "-outxml");
		build("inpathTesting");
		String resource = expectedOutputDir + File.separator + "META-INF" + File.separator + "aop-ajc.xml";
		File f = new File(resource);
		assertTrue("expected file " + resource + " to exist but it did not", f.exists());
	}

	public void testAspectsRecordedOnlyOnceInState() {
		configureNonStandardCompileOptions("inpathTesting", "-outxml");
		build("inpathTesting");
		AjState state = getState();
		Map<String,char[]> m = state.getAspectNamesToFileNameMap();
		assertEquals("Expected only one aspect recored in the state but found " + m.size(), 1, m.size());
		build("inpathTesting");
		m = state.getAspectNamesToFileNameMap();
		assertEquals("Expected only one aspect recored in the state but found " + m.size(), 1, m.size());
	}

	private AjState getState() {
		// get hold of the state for this project - expect to find one
		AjState state = IncrementalStateManager.retrieveStateFor(inpathTestingDir);
		assertNotNull("expected to find AjState for build config " + inpathTestingDir + " but didn't", state);
		return state;
	}

	private void addInpathEntry(String entry) {
		if (entry == null) {
			configureInPath("inpathTesting", (Set)null);
			return;
		}
		File f = new File(entry);
		Set<File> s = new HashSet<>();
		s.add(f);
		configureInPath("inpathTesting", s);
	}

	/**
	 * Sends all output to the same directory
	 */
	private static class SingleDirOutputLocMgr implements IOutputLocationManager {

		private File classOutputLoc;
		private File resourceOutputLoc;
		private String testProjectOutputPath;
		private List<File> allOutputLocations;
		private File outputLoc;

		public SingleDirOutputLocMgr(String testProjectPath) {
			this.testProjectOutputPath = testProjectPath + File.separator + "bin";
			outputLoc = new File(testProjectOutputPath);

			allOutputLocations = new ArrayList<>();
			allOutputLocations.add(outputLoc);
		}

		@Override
		public File getOutputLocationForClass(File compilationUnit) {
			return outputLoc;
		}

		@Override
		public Map<File,String> getInpathMap() {
			return Collections.emptyMap();
		}


		@Override
		public File getOutputLocationForResource(File resource) {
			return outputLoc;
		}

		@Override
		public List<File> getAllOutputLocations() {
			return allOutputLocations;
		}

		@Override
		public File getDefaultOutputLocation() {
			return outputLoc;
		}

		@Override
		public void reportFileWrite(String outputfile, int filetype) {
		}

		@Override
		public void reportFileRemove(String outputfile, int filetype) {
		}

		@Override
		public String getSourceFolderForFile(File sourceFile) {
			return null; // no impl
		}

		@Override
		public int discoverChangesSince(File dir, long buildtime) {
			return 0; // no impl
		}
	}

}
